<!DOCTYPE html>
<html lang="zh-CN">

<head>
  <meta charset="UTF-8">
  <title>左右对称音频律动外凸波峰圆环</title>
  <style>
    html,
    body {
      margin: 0;
      background: #000;
      height: 100%;
      display: flex;
      justify-content: center;
      align-items: center;
      flex-direction: column
    }

    canvas {
      border-radius: 8px;
      margin-bottom: 12px
    }

    button {
      padding: 8px 20px;
      font-size: 16px;
      border: none;
      border-radius: 4px;
      background: #0ff;
      color: #111;
      cursor: pointer
    }

    canvas {
      position: absolute;
    }

    #playBtn {
      position: absolute;
      top: 10px;
    }
  </style>
</head>

<body>

  <audio id="audio" src="1.mp3" crossorigin="anonymous"></audio>
  <button id="playBtn">播放 / 暂停</button>
  <canvas style="transform: rotate(-90deg);" id="c" width="600" height="600"></canvas>
  <canvas id="d" width="600" height="600" style="transform: rotate(-90deg);"></canvas>
  <script>
    /* ---------------- Web Audio ---------------- */
    let analyser, dataArray;
    async function initAudio() {
      const audio = document.getElementById('audio');
      const actx = new (window.AudioContext || window.webkitAudioContext)();
      const src = actx.createMediaElementSource(audio);
      analyser = actx.createAnalyser();
      analyser.fftSize = 256;
      dataArray = new Uint8Array(analyser.frequencyBinCount);
      src.connect(analyser);
      analyser.connect(actx.destination);

      document.getElementById('playBtn').onclick = () => {
        if (actx.state === 'suspended') actx.resume();
        audio.paused ? audio.play() : audio.pause();
      };
    }
    initAudio();
    /* ---------------- 内凹波峰 ---------------- */
    class Dent {
      constructor(center, amp, width, totalFrames = 90) {
        this.center = center;   // 角度
        this.amp = amp;      // 负振幅
        this.width = width;    // 底宽
        this.totalF = totalFrames;
        this.frame = 0;
        this.phase = 0;
      }
      /* 平滑淡入淡出 */
      envelope(t) {
        return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
      }
      currentAmp() {
        const t = this.frame / this.totalF;
        return this.amp * Math.abs(Math.cos(this.phase)) * this.envelope(1 - Math.abs(1 - t * 2));
      }
      update() {
        this.phase += 0.05;
        this.frame++;
        return this.frame >= this.totalF;
      }
    }

    /* ---------------- 圆环管理 ---------------- */
    class RingWave1 {
      constructor(cx, cy, baseR) {
        this.cx = cx;
        this.cy = cy;
        this.baseR = baseR;
        this.dents = [];
      }

      /* 一次性添加左右对称的 2 个内凹波峰 */
      addSymmetricDent(leftCenter, amp, width, frames) {
        const rightCenter = 2 * Math.PI - leftCenter;
        this.dents.push(new Dent(leftCenter, amp, width, frames));
        this.dents.push(new Dent(rightCenter, amp, width, frames));
      }

      updateAndDraw(ctx) {
        ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
        ctx.beginPath();
        const pts = 360;
        for (let i = 0; i <= pts; i++) {
          const angle = (i / pts) * Math.PI * 2;
          let pulse = 0;

          /* 叠加所有内凹波峰 */
          this.dents.forEach(d => {
            const dist = Math.abs(angle - d.center);
            const wrap = Math.min(dist, Math.PI * 2 - dist);
            if (wrap < d.width) {
              const shape = Math.cos(wrap / d.width * Math.PI / 2);
              pulse += shape * d.currentAmp(); // amp 是负数 → 内凹
            }
          });

          const r = this.baseR + pulse;
          const x = this.cx + Math.cos(angle) * r;
          const y = this.cy + Math.sin(angle) * r;
          i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
        }
        ctx.closePath();

        /* 内→外红色减半→橘黄渐变 */
        const g = ctx.createRadialGradient(this.cx, this.cy, 0, this.cx, this.cy, this.baseR + 50);
        g.addColorStop(0, 'rgb(0,0,0)');      // 内红
        g.addColorStop(0.5, 'rgb(0,0,0)');    // 红减半
        g.addColorStop(1, 'rgb(0,0,0)');    // 外橘黄
        ctx.fillStyle = g;
        ctx.fill();
        ctx.strokeStyle = '#fff';
        ctx.lineWidth = 2;
        ctx.stroke();

        /* 清理生命周期结束的波峰 */
        this.dents = this.dents.filter(d => !d.update());
      }
    }

    /* ---------------- Web Audio ---------------- */


    /* ---------------- 主循环 ---------------- */
    const cvs1 = document.getElementById('d');
    const ctx1 = cvs1.getContext('2d');
    const ring1 = new RingWave1(300, 300, 50);

    function audioLoop1() {
      analyser.getByteFrequencyData(dataArray);
      const low = dataArray.slice(0, 8).reduce((a, b) => a + b, 0) / 8;
      const mid = dataArray.slice(8, 32).reduce((a, b) => a + b, 0) / 24;
      const high = dataArray.slice(32, 64).reduce((a, b) => a + b, 0) / 32;
      const vol = Math.min(1, (low + mid + high) / 3 / 255);
      let add = 0;    // 左侧随机角度（弧度）

      /* 内凹波峰：音量阈值 + 数量上限 */
      if (vol > 0.15 && ring1.dents.length < 160) {
        if (vol > 0.5 && vol <= 0.74) {
          add = -5; // 触发整体放大动画
          /* 其余对称波峰生成保持不变 */
        }
        if (vol > 0.74) {
          add = -2; // 触发整体放大动画
          /* 其余对称波峰生成保持不变 */
        }



        const leftCenter = Math.random() * Math.PI * 2;          // 左侧随机角度
        const amp = -(1 + vol * 2) + add;                     // 负值 → 内凹
        const width = 0.08 + Math.random() * 0.1;            // 随机底宽
        const frames = 60 + Math.floor(Math.random() * 60);  // 随机时长
        ring1.addSymmetricDent(leftCenter, amp, width, frames);
      }
    }

    function loop1() {
      audioLoop1();
      ring1.updateAndDraw(ctx1);
    }
    // loop1();
  </script>
  <script>
    /* ---------------- 外凸波峰 ---------------- */
    class Bump {
      constructor(center, amp, width, totalFrames = 90) {
        this.center = center;
        this.amp = amp;
        this.width = width;
        this.totalF = totalFrames;
        this.frame = 0;
        this.phase = 0;
      }
      /* 缓入缓出曲线 */
      ease(t) {
        return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
      }
      currentAmp() {
        const t = this.frame / this.totalF;          // 0→1
        const envelope = this.ease(1 - Math.abs(1 - t * 2)); // 0→1→0
        return this.amp * Math.abs(Math.cos(this.phase)) * envelope;
      }
      update() {
        this.phase += 0.11;
        this.frame++;
        return this.frame >= this.totalF;
      }
    }

    /* ---------------- 圆环管理 ---------------- */
    class RingWave {
      constructor(cx, cy, baseR) {
        this.cx = cx;
        this.cy = cy;
        this.baseR = baseR;
        this.bumps = [];
        this.scale = 1;          // 当前整体缩放
        this.targetScale = 1;    // 目标缩放
      }




      /* 核心：一次性添加左右对称的一对波峰
         leftCenter  取左侧随机角度，右侧自动镜像
         amp、width、cycles  左右完全一致
      */
      addLeftRightPair(leftCenter, amp, width, cycles) {
        const rightCenter = 2 * Math.PI - leftCenter;  // 镜像角度
        this.bumps.push(new Bump(leftCenter, amp, width, cycles));
        this.bumps.push(new Bump(rightCenter, amp, width, cycles));
      }

      updateAndDraw(ctx) {
        ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);

        /* 1. 创建径向渐变：内圆→外圆 */
        const g = ctx.createRadialGradient(
          this.cx, this.cy, 0,          // 起点：圆心
          this.cx, this.cy, this.baseR + 50  // 终点：略大于最大凸起
        );
        g.addColorStop(0, 'rgba(0,  0,  0, 1)');   // 内：红色
        g.addColorStop(0.5, 'rgba(0,  0,  0, 0.5)'); // 中：红色减半
        g.addColorStop(1, 'rgba(0,165,  0, 1)');   // 外：橘黄色

        /* 2. 其余绘制不变 */
        ctx.beginPath();
        const pts = 360;
        for (let i = 0; i <= pts; i++) {
          const angle = (i / pts) * Math.PI * 2;
          let pulse = 0;
          this.bumps.forEach(b => {
            const dist = Math.abs(angle - b.center);
            const wrap = Math.min(dist, Math.PI * 2 - dist);
            if (wrap < b.width) {
              const shape = Math.abs(Math.cos(wrap / b.width * Math.PI / 2));
              pulse += shape * b.currentAmp();
            }
          });
          const r = this.baseR + pulse;
          const x = this.cx + Math.cos(angle) * r;
          const y = this.cy + Math.sin(angle) * r;
          i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
        }
        ctx.closePath();

        /* 3. 用渐变填充 */
        ctx.fillStyle = g;
        ctx.fill();

        /* 4. 再描边（可选） */
        ctx.strokeStyle = '#fff';
        ctx.lineWidth = 2;
        ctx.stroke();

        /* 5. 更新生命周期 */
        this.bumps = this.bumps.filter(b => !b.update());
      }
    }



    /* ---------------- 主循环 ---------------- */
    const cvs = document.getElementById('c');
    const ctx = cvs.getContext('2d');
    const ring = new RingWave(300, 300, 60);

    function audioLoop() {
      if (!analyser) return;
      analyser.getByteFrequencyData(dataArray);

      /* 三段能量平均 → 音量 0~1 */
      const low = dataArray.slice(0, 8).reduce((a, b) => a + b, 0) / 8;
      const mid = dataArray.slice(8, 32).reduce((a, b) => a + b, 0) / 24;
      const high = dataArray.slice(32, 64).reduce((a, b) => a + b, 0) / 32;
      const vol = Math.min(1, (low + mid + high) / 3 / 255);
      // console.log(vol)
      /* 音量阈值触发 + 数量保护 */
      let add = 0;    // 左侧随机角度（弧度）
      if (vol > 0.15 && ring.bumps.length < 160) {
        if (vol > 0.5 && vol <= 0.74) {
          add = 20; // 触发整体放大动画
          /* 其余对称波峰生成保持不变 */
        }
        if (vol > 0.74) {
          add = 5; // 触发整体放大动画
          /* 其余对称波峰生成保持不变 */
        }
        /* ===== 随机给值：左侧随机，右侧自动镜像 ===== */
        const leftCenter = Math.random() * Math.PI * 2;
        const amp = 1 + vol * 3 + add;                       // 振幅随音量
        const width = 0.05 + Math.random() * 0.1;           // 随机底宽
        // const cycles     = 3 + Math.floor(Math.random() * 5);   // 随机起伏次数
        const totalFrames = 60 + Math.floor(Math.random() * 60); // 60~120 帧

        ring.addLeftRightPair(leftCenter, amp, width, totalFrames);  // 一次性加左右一对
      }
    }

    function loop() {
      audioLoop();
      ring.updateAndDraw(ctx);
      loop1()
      requestAnimationFrame(loop);
    }
    loop();
  </script>


</body>

</html>